﻿using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Collections.Specialized;
using System.Diagnostics.Contracts;
using System.Globalization;
using System.Linq;
using System.Net;
using System.Net.Sockets;
using System.Text;
using System.Text.RegularExpressions;
using System.Threading;
using System.Threading.Tasks;

namespace UPNP.IO
{
    partial class DeviceFinder
    {
        private static readonly IPEndPoint MulticastEndPoint = new IPEndPoint(IPAddress.Parse("239.255.255.250"), 1900);
        private static readonly IPEndPoint AnyEndPoint = new IPEndPoint(IPAddress.Any, 0);

        private Socket _ssdpSocket;
        private byte[] _ssdpReceiveBuffer;

        private void StartListening()
        {
            // create socket
            _ssdpSocket = new Socket(
                AddressFamily.InterNetwork,
                SocketType.Dgram,
                ProtocolType.Udp
                )
            {
                Blocking = false,
                Ttl = 1,
                UseOnlyOverlappedIO = true,
                MulticastLoopback = false,
            };

            _ssdpSocket.Bind(AnyEndPoint);

            _ssdpSocket.SetSocketOption(
                SocketOptionLevel.IP,
                SocketOptionName.AddMembership,
                new MulticastOption(MulticastEndPoint.Address)
                );

            if (_log.IsInfoEnabled)
            {
                _log.Info("SSDP Socket bound to: " + _ssdpSocket.LocalEndPoint);
            }

            // start listening
            ReceiveResponseRecursive();
        }

        private void SendDiscover(string typeId = "upnp:rootdevice")
        {
            Contract.Requires(!string.IsNullOrEmpty(typeId));

            if (_log.IsInfoEnabled)
            {
                _log.Info("Sending SSDP discovery for type id: " + typeId);
            }

            // prepare message
            var msg = string.Format(
                Properties.Messages.SsdpDiscover,
                MulticastEndPoint.Address,
                MulticastEndPoint.Port,
                typeId
                );

            var data = Encoding.ASCII.GetBytes(msg);

            // send message
            for (int i = 0; i < 3; i++)
            {
                if (i > 0)
                    Thread.Sleep(50);

                var async = _ssdpSocket.BeginSendTo(
                    data,
                    0,
                    data.Length,
                    SocketFlags.None,
                    MulticastEndPoint,
                    o =>
                    {
                        var r = _ssdpSocket.EndSendTo(o);

                        if (_log.IsWarnEnabled && (r != data.Length))
                        {
                            _log.WarnFormat(
                                "Sent SSDP discovery request length mismatch: {0} != {1} (expected)",
                                r,
                                data.Length
                                );
                        }
                    },
                    null
                    );

                async.AsyncWaitHandle.WaitOne();
            }
        }

        #region response handling

        private void HandleResponse(EndPoint sender, string response)
        {
            Contract.Requires(sender != null);
            Contract.Requires(response != null);

            // debug
            if (_log.IsDebugEnabled)
                _log.Debug("Received response from " + sender + ": " + response);

            // handle empty response
            if (string.IsNullOrEmpty(response))
            {
                if (_log.IsWarnEnabled)
                    _log.Warn("Received empty response from " + sender);

                return;
            }

            // parse response
            var res = new TcpIncomingResponse(sender, response);

            try
            {
                var server = res.Headers["SERVER"];
                var deviceId = res.Headers["USN"];
                var location = new Uri(res.Headers["LOCATION"]);

                // device is already known?
                // if not, reserve an empty slot
                if (!_devices.TryAdd(deviceId, null))
                    return;

                // inform
                if (_log.IsInfoEnabled)
                {
                    _log.InfoFormat(
                        "Device {0} ({1}) found at: {2}; Parsing device...",
                        deviceId,
                        server,
                        location
                        );
                }

                // add device
                var t = new Task<DeviceSchema>(
                        // parse the device schema
                        () => new DeviceSchema(location)
                    );

                t.ContinueWith(
                    // create the root device from the device schema
                        o => o.Result.CreateRootDevice(),
                        TaskContinuationOptions.NotOnFaulted
                    ).ContinueWith(
                        o =>
                        {
                            // finally add device to device list
                            // use device id from SSDP response (just to be sure)
                            // make sure the device is still online
                            if (!o.Result.ConnectionLost)
                            {
                                // register for connection lost
                                RootDevice removed;

                                o.Result.OnConnectionLost += (s, e) => _devices.TryRemove(deviceId, out removed);

                                // try to update the device
                                // connection might already be lost now
                                _devices.TryUpdate(deviceId, o.Result, null);
                            }
                        },
                        TaskContinuationOptions.NotOnFaulted
                    ).ContinueWith(
                        o =>
                        {
                            // if there was an exception, then
                            // remove the device
                            RootDevice r;

                            _devices.TryRemove(deviceId, out r);
                        },
                        TaskContinuationOptions.OnlyOnFaulted
                    );
                
                t.Start();
            }
            catch (Exception x)
            {
                if (_log.IsErrorEnabled)
                    _log.Error("Failed to parse reponse headers from " + sender, x);
            }
        }

        private void ReceiveResponseRecursive(IAsyncResult r = null)
        {
            Contract.Requires(r == null || _ssdpReceiveBuffer != null);
            Contract.Ensures(_ssdpReceiveBuffer != null);

            // check if there is an response
            // or this is the first call
            if (r != null)
            {
                // complete read
                EndPoint senderEndPoint = AnyEndPoint;

                var read = _ssdpSocket.EndReceiveFrom(
                    r,
                    ref senderEndPoint
                    );

                // parse result
                var resBuf = _ssdpReceiveBuffer;    // make sure we don't reuse the reference

                new Task(
                    () => HandleResponse(senderEndPoint, Encoding.ASCII.GetString(resBuf, 0, read))
                    ).Start();
            }

            // trigger the next cycle
            // tail recursion
            EndPoint recvEndPoint = AnyEndPoint;
            _ssdpReceiveBuffer = new byte[4096];

            _ssdpSocket.BeginReceiveFrom(
                _ssdpReceiveBuffer,
                0,
                _ssdpReceiveBuffer.Length,
                SocketFlags.None,
                ref recvEndPoint,
                ReceiveResponseRecursive,
                null
                );
        }

        #endregion
    }
}
